package com.vincent.redispractise.utils;


import cn.hutool.core.lang.UUID;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.vincent.redispractise.part01.utils.RedisLogicExpireData;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Slf4j
@Component
public class CacheService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private static String THREADPREFIX = UUID.randomUUID(false).toString()+"_";

    private static  final DefaultRedisScript<Long> UNLOCK_SCRIPT ;
    static{
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }
    //保存永久对象
    public void set(String key, String value){
        stringRedisTemplate.opsForValue().set(key, value);
    }

    //保存对象并设置过期时间
    public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
    }
    //保存对象设置逻辑过期时间
    public void setWithLogicExpire(String key, Object value, Long time, TimeUnit unit){
        RedisLogicExpireData expireData = new RedisLogicExpireData().setLocalDateTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time))).setData(value);
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(expireData));
    }
    //简单查询,对象反序列化成
    public <T> T get(String key, Class<T> type){
        return Optional.ofNullable(stringRedisTemplate.opsForValue().get(key)).map(data -> {
            T vo = null;
            try {
                vo = JSONUtil.toBean(data, type);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return vo;
        }).orElse(null);
    }

    //获取互斥锁
    public Boolean tryLock(String key, Object value, Long time, TimeUnit unit){
        Boolean flag = false;
        try {
            flag = stringRedisTemplate.opsForValue().setIfAbsent(key,JSONUtil.toJsonStr(value),time,unit);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return flag;
    }
    //释放互斥锁
    public Boolean delLock(String key, Object value){
        Boolean flag = false;
        try {
            flag = stringRedisTemplate.delete(key);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return flag;
    }

    /**
     * @Des 封装缓存穿透(pass through)+缓存击穿互斥策略
     * @Date 2023/2/23 15:22
     * @Param  * @param cacheKey 缓存key
     * @Param  * @param mutexLockKey 互斥锁key
     * @Param  * @param type 返回的Class
     * @Param  * @param time 缓存时长
     * @Param  * @param id  缓存时长单位
     * @Param  * @param function 函数式编程，封装查询数据库返回实体的操作
     * @Return
     */
    public <T,ID> T getWithMutexLock(String cacheKey,String mutexLockKey, Class<T> type, Long time, TimeUnit unit,ID id, Function<ID,T> function){
        //获取缓存
        T cacheData = this.get(cacheKey,type);
        /*如果缓存存在，此情况下
            1.1缓存穿透只需要返回缓存值
            1.2缓存击穿：
                互斥策略：返回缓存
        */
        if (cacheData != null) {
            return cacheData;
        }
        /*如果缓存不存再，此情况下
            1.1缓存穿透：如果查询数据库没有，需要处理为空对象(其实处理缓存穿透的逻辑包含在缓存击穿的逻辑里面，无非是查询数据库的时候没有记录的处理方式)
            1.2缓存击穿：
                互斥策略：获取互斥锁
                    成功：重建缓存（）
                    失败：休眠一会，重新查询缓存，如果没有则重建缓存
        */
        //获取互斥锁
        Boolean lock = this.tryLock(mutexLockKey, 0, 10L, TimeUnit.SECONDS);
        //成功：重建缓存
        if (lock) {
            log.info("主线程使用互斥锁策略构建缓存");
            //如果数据库为空，设置空对象防止缓存穿透
            //查询操作数据库，封装一种行为函数，使用函数式接口
            T apply = function.apply(id);
//            Shop shopVo = Optional.ofNullable(query().eq("id", id).one()).orElse(new Shop());
            this.set(cacheKey, apply, time, unit);
            //释放锁
            this.delLock(mutexLockKey, null);
            return apply;
        } else {
            try {
                Thread.sleep(2L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //重试
            return getWithMutexLock(cacheKey,mutexLockKey,type,time,unit,id,function);
        }
    }
    /**
     * @Des 封装缓存穿透(pass through)+缓存击穿逻辑过期策略
     * @Date 2023/2/23 15:22
     * @Param  * @param cacheKey 缓存key
     * @Param  * @param mutexLockKey 互斥锁key
     * @Param  * @param type 返回的Class
     * @Param  * @param time 缓存时长
     * @Param  * @param id  缓存时长单位
     * @Param  * @param function 函数式编程，封装查询数据库返回实体的操作
     * @Return
     */
    public <T,ID> T getWithLogicExp(String cacheKey,String mutexLockKey, Class<T> type,Long time, TimeUnit unit,ID id, Function<ID,T> function){
        //逻辑过期，都能兼容
        RedisLogicExpireData expireData = this.get(cacheKey, RedisLogicExpireData.class);
        /*如果缓存存在，此情况下
            1.1缓存穿透只需要返回缓存值
            1.2缓存击穿：
                逻辑过期：需要判断缓存是否逻辑有效，负责需要重建缓存,然后返回旧值
        */
        if (expireData != null) {
            //缓存穿透+逻辑过期
            //如果是逻辑过期，存入redis的值需要封装为有逻辑过期时间的实体，用来判断逻辑过期时间（RedisLogicExpireData）
            if (expireData.getLocalDateTime().isAfter(LocalDateTime.now())) {
                return JSONUtil.toBean((JSONObject)expireData.getData(),type);
            } else {
                /*逻辑已过期，获取互斥锁
                    成功：开启子线程，查询数据库，更新缓存（这里考虑更新缓存还是删除缓存），主线程返回旧数据
                    失败：返回旧数据
                    返回值：不管成功失败，都是需要返回旧值；
                */
                Boolean lock = this.tryLock(mutexLockKey, 0, 10L, TimeUnit.SECONDS);
                if (lock) {
                    //Todo:考虑使用现成池，可控线程
                    new Thread(() -> {
                        log.info("子线程使用逻辑过期策略构建缓存");
                        //查询数据库
                        try {
                            //如果数据库为空，设置空对象防止缓存穿透
//                            Shop shop = Optional.ofNullable(query().eq("id", id).one()).orElse(new Shop());
                            T apply = function.apply(id);
                            this.setWithLogicExpire(cacheKey, apply, time, unit);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        //释放锁
                        this.delLock(mutexLockKey, null);
                    }
                    ).start();
                } else {
                    //获取锁失败，说明此时已经有线程在重建此缓存，返回旧值
                }
                return JSONUtil.toBean((JSONObject)expireData.getData(),type);
            }
        } else {
            /*如果缓存不存再，此情况下(理论上逻辑过期不会出现这种没有缓存情况，需要提前预热，这里做一个容错处理)
                1.1缓存穿透：如果查询数据库没有，需要处理为空对象(其实处理缓存穿透的逻辑包含在缓存击穿的逻辑里面，无非是查询数据库的时候没有记录的处理方式)
                1.2缓存击穿：
                    逻辑过期：首次建立缓存，获取互斥锁
                        成功：重建缓存，并且返回此值（逻辑过期需要事先建立缓存）
                        失败：休眠一会，重新查询缓存,递归调用这个方法（包括有缓存，无缓存）
            */
            Boolean lock = this.tryLock(mutexLockKey, 0, 10L, TimeUnit.SECONDS);
            //成功：重建缓存
            if (lock) {
                //如果数据库为空，设置空对象防止缓存穿透
//                Shop shopVo = Optional.ofNullable(query().eq("id", id).one()).orElse(new Shop());
                T apply = function.apply(id);
                this.setWithLogicExpire(cacheKey, apply, time, unit);
                //释放锁
                this.delLock(mutexLockKey, null);
                return apply;
            } else {
                try {
                    Thread.sleep(2L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //重试
                return getWithLogicExp(cacheKey,mutexLockKey,type,time,unit,id,function);
            }
        }
    }
    /**
     * @Des 获取互斥锁[处理锁超时释放的问题]-删除其他线程设置的锁-添加线程标识
     * @Date 2023年3月5日16:26:10
     * @desc：多个jvm可能获取到的线程id相同，所以需要再前面获取每台机器的UUID（严格来讲UUID在分布式环境下也可能重复，几率很小罢了）
     * @Return
     */
    //获取互斥锁
    public Boolean tryLock1(String key,Long time, TimeUnit unit){
        Boolean flag = false;
        try {
            //将value设置为带有线程标识的值，方便在释放是对比：UUID+线程ID
            long id = Thread.currentThread().getId();
            flag = stringRedisTemplate.opsForValue().setIfAbsent(key,THREADPREFIX+id,time,unit);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return flag;
    }
    /**
     * @Des 获取互斥锁[处理锁超时释放的问题]-删除其他线程设置的锁-添加线程标识
     * @Date 2023年3月5日16:26:10
     * @desc：释放锁时判断此锁是否为此线程设置的，避免释放别人的锁，但是判断和释放非原子性操作，线程不安全（下面用luna脚本解决）
     * @Return
     */
    public Boolean delLock1(String key){
        Boolean flag = false;
        try {
            //将value设置为带有线程标识的值，方便在释放是对比：UUID+线程ID
            long id = Thread.currentThread().getId();
            String value = stringRedisTemplate.opsForValue().get(key);
            //非原子操作，线程不安全
            if((THREADPREFIX+id).equals(value)){
                flag = stringRedisTemplate.delete(key);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return flag;
    }
    /**
     * @Des 释放锁时判断此锁是否为此线程设置的，避免释放别人的锁[lua脚本解决]
     * @Date 2023年3月5日16:26:10
     * @desc：
     * @Return
     */
    public Long delLock2(String key){
        String value = THREADPREFIX + Thread.currentThread().getId();
        Long execute = stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(key), value);
        System.out.println(execute);
        return execute;
    }
}
